import fs from "fs-extra";
import fg from "fast-glob";
import path from "path";
import { Project, SyntaxKind, Node, Type } from "ts-morph";
import { Nullable } from "../typings/index.js";
import _ from "lodash";
import { toolchain } from "../toolchain.js";

export class CodePatcher {
    private crtLogs: string[] = [];

    public async organizeImports(): Promise<void> {
        if (toolchain.opts.output && fs.existsSync(toolchain.opts.output)) {
            await fs.unlink(toolchain.opts.output);
        }
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');

        const project = new Project({
            tsConfigFilePath: path.join(tsRoot, 'tsconfig.json')
        });

        // 'System/data/PetData.ts'
        // const tss = await fg('System/activity/challenge/JJC/JingJiChangView.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] });
        const tss = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts', 'root.ts'] });
        for (const ts of tss) {
            const tsf = path.join(tsRoot, ts);
            this.crtLogs.length = 0;
            const src = project.getSourceFileOrThrow(tsf);
            src.organizeImports();
        }
        await project.save();
    }

    public async patchAny(): Promise<void> {
        if (toolchain.opts.output && fs.existsSync(toolchain.opts.output)) {
            await fs.unlink(toolchain.opts.output);
        }
        const tsRoot = path.join(toolchain.opts.projectRoot, 'TsScripts');

        const project = new Project({
            tsConfigFilePath: path.join(tsRoot, 'tsconfig.json')
        });

        // 'System/data/PetData.ts'
        // const tss = await fg('System/activity/fuLi/DiZheZhiLuBuyView.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] });
        const tss = await fg('**/*.ts', { cwd: tsRoot, ignore: ['**/*.d.ts'] });
        for (const ts of tss) {
            const tsf = path.join(tsRoot, ts);
            this.crtLogs.length = 0;
            const src = project.getSourceFileOrThrow(tsf);
            let modifyCnt = 0;
            const clss = src.getClasses();
            clss.forEach((v) => {
                modifyCnt += this.patchAnyInNode(v);
            });
            if (this.crtLogs.length > 0 && toolchain.opts.output) {
                await fs.appendFile(toolchain.opts.output, tsf + '\n', 'utf-8');
                await fs.appendFile(toolchain.opts.output, this.crtLogs.join('\n') + '\n', 'utf-8');
            }
            if (modifyCnt > 0) {
                // console.log(modifyCnt + ' modified :', ts);
                await src.save();
            }
        }        
    }

    private patchAnyInNode(node: Node): number {
        let modifyCnt = 0;
        if (node.isKind(SyntaxKind.VariableDeclaration) || node.isKind(SyntaxKind.PropertyDeclaration)) {
            if (!node.getNameNode().isKind(SyntaxKind.ArrayBindingPattern)) {
                const tt = node.getType().getText();
                // console.log(node.getName(), tt);
                if (tt == 'any[]' || tt == 'any') {
                    // console.log('any[] detected!', node.getName(), tt);
                    const refers = node.findReferencesAsNodes();
                    let assignTypes: string[] = [], callArgTypes: string[] = [], pushTypes: string[] = [];
                    for (const v of refers) {
                        if (v.isKind(SyntaxKind.Identifier)) {
                            // 通过赋值推断
                            const at = this.inferFromAssignment(v);
                            if (at) {
                                if (at != 'null[]' && at != 'undefined[]') {
                                    assignTypes.push(at);
                                }
                            } else {
                                // 通过CallExpression的参数推断
                                const cat = this.inferFromCallArg(v);
                                if (cat) {
                                    callArgTypes.push(cat);
                                } else if (tt == 'any[]') {
                                    // 通过node.push推断
                                    const pt = this.inferFromPush(v);
                                    if (pt) {
                                        pushTypes.push(...pt);
                                    }
                                }
                            }
                        }
                    }
    
                    let typeStr = _.uniq(assignTypes).join(' | ') || _.uniq(callArgTypes).join(' | ');
                    if (!typeStr) {
                        pushTypes = _.uniq(pushTypes);
                        if (pushTypes.length > 1) {
                            typeStr = '(' + pushTypes.join(' | ') + ')[]';
                        } else if(pushTypes.length == 1) {
                            typeStr = pushTypes[0] + '[]';
                        }
                    }
    
                    if (typeStr) {
                        this.crtLogs.push(`${node.getName()}: ${typeStr}`);
                        node.setType(typeStr);
                        modifyCnt++;
                    } else {
                        this.crtLogs.push(`${node.getName()}: ???`);
                    }
                }
            }
        } else if (node.isKind(SyntaxKind.MethodDeclaration)) {
            if (node.getReturnType().getText() == 'any') {
                this.crtLogs.push(`${node.getName()}: ???`);
            }
        } else {
            const children = node.getChildren();
            for (const child of children) {
                modifyCnt += this.patchAnyInNode(child);
            }
        }
        return modifyCnt;
    }

    private inferFromAssignment(node: Node): Nullable<string> {
        const be = node.getParentIfKind(SyntaxKind.BinaryExpression);
        if (!be || !be.getOperatorToken().isKind(SyntaxKind.EqualsToken)) return null;

        const left = be.getLeft();
        const right = be.getRight();
        if (right == node) {
            return this.eusureNotAnyType(left.getType());
        }
        if (left == node) {
            return this.eusureNotAnyType(right.getType());
        }
        return null;
    }

    private inferFromCallArg(v: Node): Nullable<string> {
        const ce = v.getParentIfKind(SyntaxKind.CallExpression);
        if (!ce) return null;
        const exp = ce.getExpressionIfKind(SyntaxKind.PropertyAccessExpression);
        if (exp) {
            const exprNode = exp.getExpression()
            if (exp.getName() == 'push' && exprNode.getType().isArray()) {
                // 类似xxx.push(v)，通过xxx的类型推断v的类型
                // eg. System/data/PetData.ts group
                return this.eusureNotAnyType(exprNode.getType().getArrayElementTypeOrThrow());
            } else {
                const expNameNode = exp.getNameNode();
                // 假如v是该方法调用的参数，通过方法签名判断
                // eg. System/unit/hero/HeroController.ts serverPathLists
                const args = ce.getArguments();
                for (let i = 0, len = args.length; i < len; i++) {
                    if (args[i] == v) {
                        const defs = expNameNode.getDefinitionNodes();
                        for (const def of defs) {
                            if (!def.isKind(SyntaxKind.MethodDeclaration)) continue;
                            const params = def.getParameters();
                            const p = params[i];
                            if (p) {
                                return this.eusureNotAnyType(p.getType());
                            }
                        }
                        break;
                    }
                }
            }
        }
        return null;
    }

    private inferFromPush(node: Node): Nullable<string[]> {
        // 通过node.push推断
        const pae = node.getParentIfKind(SyntaxKind.PropertyAccessExpression);
        if (pae && pae.getName() == 'push') {
            const ce = pae.getParentIfKind(SyntaxKind.CallExpression);
            if (ce) {
                const pushTypes: string[] = [];
                const args = ce.getArguments();
                for (const arg of args) {
                    const argType = this.eusureNotAnyType(arg.getType());
                    if (argType && argType != 'null' && argType != 'undefined') {
                        pushTypes.push(argType);
                    }
                }
                return pushTypes;
            }
        }
        return null;
    }

    private eusureNotAnyType(type: Type): Nullable<string> {
        // let t = type.getText(undefined, TypeFormatFlags.UseFullyQualifiedType);
        // if (!t.startsWith('GameConfig.') && !t.startsWith('Protocol.')) {
        //     t = type.getText(undefined, TypeFormatFlags.WriteClassExpressionAsTypeLiteral);
        // }
        const t = type.getText().replace(/^import\(.+?\)\./, '');
        if (t.search(/\bany\b/) < 0) {
            return t;
        }
        return null;
    }
}
